<template>
  <div class="kb-docs-page">
    <div class="kb-docs-layout">
      <!-- 左侧：子菜单 -->
      <aside class="kb-sidemenu">
        <!-- <div class="menu-title">子菜单</div> -->
        <el-menu :default-active="activeMenu" class="menu" @select="onMenuSelect">
          <el-menu-item index="docs">文档与信息</el-menu-item>
          <el-menu-item index="recall">召回测试</el-menu-item>
        </el-menu>
      </aside>

      <!-- 右侧：原有内容 -->
      <main class="kb-main">
        <template v-if="activeMenu==='docs'">
          <div class="head">
            <el-button link type="primary" @click="$router.back()">返回</el-button>
            <h3>知识库文档</h3>
            <div class="spacer"/>
            <el-button type="primary" class="add-doc-btn" @click="goCreate">
              <el-icon><Plus /></el-icon>
              <span>添加文档</span>
            </el-button>
          </div>

          <!-- 知识库信息卡片 -->
          <el-card v-if="kbInfo" class="kb-info" shadow="hover">
            <div class="kb-info-row">
              <div class="kb-title">知识库:{{ kbInfo.name }}</div>
              <div class="kb-desc" v-if="kbInfo.description">{{ kbInfo.description }}</div>
            </div>
            <div class="kb-stats">
              <div class="stat">
                <div class="label">文档总数</div>
                <div class="value">{{ total }}</div>
              </div>
              <div class="stat">
                <div class="label">默认嵌入模型</div>
                <div class="value">{{ firstDocEmbedding || '-' }}</div>
              </div>
            </div>
          </el-card>

          <el-table :data="docs" v-loading="loading" style="width: 100%">
            <el-table-column label="文档" min-width="280">
              <template #default="{ row }">
                <el-input v-if="editingId===row.id" v-model="editName" size="small" @keyup.enter="saveName(row)"/>
                <template v-else>
                  <span class="doc-name" @dblclick="startEdit(row)">{{ row.doc_name }}</span>
                </template>
              </template>
            </el-table-column>
            <el-table-column prop="vector_count" label="向量数" width="120"/>
            <el-table-column prop="embedding_model_name" label="嵌入模型" min-width="200"/>
            <el-table-column label="分块" min-width="220">
              <template #default="{ row }">
                <span v-if="row.chunk_mode==='length'">长度(size={{ row.chunk_params?.chunk_size || row.chunk_params?.size }}, overlap={{ row.chunk_params?.overlap || row.chunk_params?.chunk_overlap }})</span>
                <span v-else-if="row.chunk_mode==='delimiter'">分隔符'{{ (row.chunk_params && (row.chunk_params.delimiter || row.chunk_params.separator)) || '' }}'</span>
                <span v-else-if="row.chunk_mode==='sentence'">按句子</span>
                <span v-else>按段落</span>
              </template>
            </el-table-column>
            <el-table-column label="启用" width="120" align="center">
              <template #default="{ row }">
                <el-switch
                  v-model="row.is_enabled"
                  :active-value="true"
                  :inactive-value="false"
                  @change="toggleDocEnabled(row)"
                />
              </template>
            </el-table-column>
            <el-table-column label="操作" width="300">
              <template #default="{ row }">
                <template v-if="editingId!==row.id">
                  <el-button size="small" @click="viewChunks(row.id)">查看分块</el-button>
                  <el-button size="small" type="primary" link @click="startEdit(row)">重命名</el-button>
                  <el-button size="small" type="danger" link @click="confirmDeleteDoc(row.id)">删除</el-button>
                </template>
                <template v-else>
                  <el-button size="small" type="primary" :disabled="!canApply(row)" @click="saveName(row)">应用修改</el-button>
                  <el-button size="small" link @click="cancelEdit(row)">取消</el-button>
                </template>
              </template>
            </el-table-column>
          </el-table>

          <div class="pager">
            <el-pagination
              background
              layout="prev, pager, next"
              :current-page="page"
              :page-size="pageSize"
              :total="total"
              @current-change="onPageChange"
            />
          </div>
        </template>

        <template v-else>
          <div class="head">
            <el-button link type="primary" @click="$router.back()">返回</el-button>
            <h3>召回测试</h3>
          </div>

          <div class="recall-layout">
            <div class="recall-main">
              <el-card class="recall-card" shadow="hover">
                <div class="recall-form">
                  <el-input
                    v-model="recallQuery"
                    type="textarea"
                    :autosize="{ minRows: 8, maxRows: 16 }"
                    maxlength="200"
                    show-word-limit
                    placeholder="请输入问题，建议简短描述"
                  />
                  <div class="recall-actions">
                    <el-button type="primary" @click="recallSearch">测试</el-button>
                  </div>
                </div>
                <div class="recall-result" v-if="currentRecall">
                  <div class="recall-item" v-if="currentRecall.hits && currentRecall.hits.length">
                    <div class="q">Q：{{ currentRecall.query }}</div>
                    <div class="hits">
                      <div class="hit-card clickable" v-for="h in currentRecall.hits" :key="h.key" @click="goToDoc(h)">
                        <div class="hit-head">
                          <div class="doc" :title="displayDocName(h)">{{ displayDocName(h) }}</div>
                          <div class="score" v-if="h.score !== undefined">{{ (h.score*100).toFixed(0) }}% ({{ h.score.toFixed(3) }})</div>
                        </div>
                        <div class="chunk">{{ h.chunk_text }}</div>
                      </div>
                    </div>
                  </div>
                  <div class="recall-empty" v-else>未找到符合条件的召回内容</div>
                </div>
              </el-card>
            </div>

            <aside class="recall-settings">
              <div class="settings-stack">
                <el-card shadow="hover">
                  <div class="settings-title">召回设置</div>
                  <el-form label-position="top" class="settings-form">
                    <el-form-item label="TopK（返回条数）">
                      <el-input-number v-model="recallTopK" :min="1" :max="50" :step="1" />
                    </el-form-item>
                    <el-form-item label="相似度阈值（0-1）">
                      <el-slider v-model="recallThreshold" :min="0" :max="1" :step="0.01" show-input />
                    </el-form-item>
                  </el-form>
                </el-card>
                <el-card shadow="hover">
                  <div class="history-title">历史召回</div>
                  <div class="history-list">
                    <div class="history-item" v-for="item in recallHistory" :key="item.id">
                      <div class="history-left" @click="currentRecall = item">
                        <div class="history-avatar">🔎</div>
                        <div class="history-main">
                          <div class="history-q" :title="item.query">Q：{{ item.query }}</div>
                        </div>
                      </div>
                      <el-button link type="danger" size="small" class="history-del" @click.stop="onDeleteHistory(item)">删除</el-button>
                    </div>
                    <div v-if="recallHistory.length===0" class="history-empty">暂无历史</div>
                  </div>
                </el-card>
              </div>
            </aside>
          </div>
        </template>
      </main>
    </div>
  </div>
</template>

<script>
import { apiService } from '@/services/api'
import { ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'

export default {
  name: 'KnowledgeDocuments',
  components: { Plus },
  data() {
    return {
      kbId: null,
      kbInfo: null,
      docs: [],
      page: 1,
      pageSize: 10,
      total: 0,
      loading: false,
      editingId: null,
      editName: '',
      recallQuery: '',
      recallHistory: [],
      currentRecall: null,
      activeMenu: 'docs',
      recallTopK: 8,
      recallThreshold: 0,
      docNameMap: {}
    }
  },
  mounted() {
    this.kbId = Number(this.$route.params.kbId)
    this.fetch()
  },
  computed: {
    firstDocEmbedding() {
      const doc = this.docs.find(d => d.embedding_model_name)
      return doc ? doc.embedding_model_name : ''
    },
    commonChunkDesc() {
      const d = this.docs.find(x => x.chunk_mode)
      if (!d) return ''
      if (d.chunk_mode === 'length') {
        const p = d.chunk_params || {}
        return `长度(size=${p.chunk_size || p.size || '-'}, overlap=${p.overlap || p.chunk_overlap || '-'})`
      }
      if (d.chunk_mode === 'delimiter') {
        const p = d.chunk_params || {}
        return `分隔符'${p.delimiter || p.separator || ''}'`
      }
      return d.chunk_mode === 'sentence' ? '按句子' : '按段落'
    }
  },
  methods: {
    async fetch() {
      this.loading = true
      try {
        const [kb, res] = await Promise.all([
          apiService.getKnowledgeBase(this.kbId),
          apiService.listDocumentsByKb(this.kbId, this.page, this.pageSize)
        ])
        this.kbInfo = kb
        this.docs = res.documents || []
        this.total = res.total_count || 0
        // build id->name map for fast lookup
        this.docNameMap = Object.fromEntries((this.docs || []).map(d => [d.id, d.doc_name]))
      } catch (e) {
        // ignore
      } finally {
        this.loading = false
      }
    },
    async toggleDocEnabled(row){
      try{
        const target = !!row.is_enabled
        await apiService.updateDocumentEnabled(row.id, target)
        this.$message.success(target ? '已启用' : '已禁用')
      }catch(e){
        // 回滚本地状态
        row.is_enabled = !row.is_enabled
        this.$message.error('更新失败')
      }
    },
    onMenuSelect(idx) {
      this.activeMenu = idx
      if (idx === 'recall') this.loadRecallHistory()
    },
    onPageChange(p) {
      this.page = p
      this.fetch()
      if (this.activeMenu === 'recall') {
        this.loadRecallHistory()
      }
    },
    viewChunks(docId) {
      this.$router.push({ name: 'document-chunks', params: { docId } })
    },
    startEdit(row) {
      this.editingId = row.id
      this.editName = row.doc_name
      this.$nextTick(() => {})
    },
    canApply(row) {
      const newName = (this.editName || '').trim()
      return this.editingId === row.id && !!newName && newName !== row.doc_name
    },
    cancelEdit() {
      this.editingId = null
      this.editName = ''
    },
    async saveName(row) {
      if (this.editingId !== row.id) return
      const newName = (this.editName || '').trim()
      if (!newName || newName === row.doc_name) {
        this.editingId = null
        return
      }
      try {
        await apiService.updateDocumentName(row.id, newName)
        this.$message.success('已更新')
        row.doc_name = newName
      } catch (e) {
        // ignore
      } finally {
        this.editingId = null
      }
    },
    async deleteDoc(docId) {
      try {
        await apiService.deleteDocument(docId)
        this.$message.success('已删除')
        this.fetch()
      } catch (e) {
        // ignore
      }
    },
    async confirmDeleteDoc(docId) {
      try {
        await ElMessageBox.confirm('确认删除该文档及其分块？', '提示', {
          confirmButtonText: '删除',
          cancelButtonText: '取消',
          type: 'warning'
        })
        await this.deleteDoc(docId)
      } catch (_) { /* canceled */ }
    },
    async recallSearch() {
      const q = (this.recallQuery || '').trim()
      if (!q) return
      try {
        const res = await apiService.searchKnowledge(this.kbId, q, this.recallTopK)
        const arr = (res && (res.results || res.hits || res.items)) || []
        const hits = arr
          .map((x, i) => ({
            key: `${x.doc_id || 'd'}-${x.chunk_index ?? i}`,
            doc_id: x.doc_id,
            doc_name: x.doc_name || this.getDocName(x.doc_id),
            chunk_index: x.chunk_index,
            chunk_text: x.chunk_text || x.text || x.content || '',
            score: typeof x.score === 'number' ? x.score : (typeof x.similarity === 'number' ? x.similarity : 0)
          }))
          .filter(h => h.score >= Number(this.recallThreshold || 0))
        this.currentRecall = { id: Date.now(), query: q, hits }
        // ensure names for hits if missing
        this.ensureDocNames(this.currentRecall.hits)
        // 同步远端历史（后端已持久化），并刷新右侧历史列表
        this.loadRecallHistory()
      } catch (e) {
        this.$message.error('召回失败')
      }
    },
    async loadRecallHistory() {
      try {
        const res = await apiService.listRecallHistory(this.kbId, 20)
        const items = (res && res.items) || []
        // 无缝兼容后端结构
        this.recallHistory = items.map(it => ({
          id: it.id,
          query: it.query_text,
          createdAt: it.created_at,
          hits: (it.hits || []).map((h, i) => ({
            key: `${h.doc_id || 'd'}-${h.chunk_index ?? i}`,
            doc_id: h.doc_id,
            doc_name: h.doc_name,
            chunk_index: h.chunk_index,
            chunk_text: h.chunk_text,
            score: h.score
          }))
        }))
      } catch (e) {
        // ignore
      }
    },
    formatDt(t){
      try{ const d=new Date(t); if(isNaN(d)) return ''; return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` }catch(_){ return '' }
    },
    goToDoc(hit){
      const id = hit && (hit.doc_id || hit.docId)
      if (id) {
        this.$router.push({ name: 'document-chunks', params: { docId: id } })
      }
    },
    getDocName(docId){
      if (!docId) return ''
      return this.docNameMap[docId] || ''
    },
    displayDocName(hit){
      const name = (hit && (hit.doc_name || this.getDocName(hit.doc_id))) || ''
      return name || (hit && hit.doc_id ? `文档#${hit.doc_id}` : '')
    },
    async ensureDocNames(hits){
      try{
        const missingIds = Array.from(new Set((hits || [])
          .filter(h => h && h.doc_id && !(h.doc_name || this.docNameMap[h.doc_id]))
          .map(h => h.doc_id)))
        const tasks = missingIds.map(async id => {
          try{
            const doc = await apiService.getDocument(id)
            if (doc && doc.id) {
              this.$set ? this.$set(this.docNameMap, doc.id, doc.doc_name) : (this.docNameMap[doc.id] = doc.doc_name)
            }
          }catch(_){ /* ignore */ }
        })
        await Promise.allSettled(tasks)
        // update hits with fetched names
        ;(hits || []).forEach(h => {
          if (h && h.doc_id && !h.doc_name) {
            const name = this.docNameMap[h.doc_id]
            if (name) h.doc_name = name
          }
        })
      }catch(_){ /* ignore */ }
    },
    async onDeleteHistory(item) {
      try {
        await apiService.deleteRecallHistory(this.kbId, item.id)
        this.$message.success('已删除')
        if (this.currentRecall && this.currentRecall.id === item.id) {
          this.currentRecall = null
        }
        this.loadRecallHistory()
      } catch (e) {
        // ignore
      }
    },
    goCreate() {
      this.$router.push({ name: 'knowledge-create', query: { kbId: this.kbId } })
    }
  }
}
</script>

<style scoped>
.kb-docs-page{ padding: 16px 24px 24px; --kb-accent: #4dabf7; --kb-accent-weak: rgba(77, 171, 247, 0.15); --kb-surface: linear-gradient(180deg, rgba(77,171,247,0.04), rgba(77,171,247,0.02)); background: var(--kb-surface); width: 100%; max-width: none; margin: 0; font-size: 15px; }

.kb-docs-layout{ display: grid; grid-template-columns: 220px 1fr; gap: 16px; align-items: start; }
@media (max-width: 1100px){ .kb-docs-layout{ grid-template-columns: 1fr; } }

.kb-sidemenu{ border: 1px solid var(--el-border-color); border-radius: 12px; background: var(--el-fill-color-blank); padding: 12px; position: sticky; top: 16px; box-shadow: 0 2px 10px rgba(16,24,40,.04); }
.kb-sidemenu .menu-title{ font-weight: 700; font-size: 14px; margin-bottom: 8px; }
.kb-sidemenu .menu{ border-right: none; }
.kb-sidemenu :deep(.el-menu-item){ border-radius: 8px; margin-bottom: 6px; }
.kb-sidemenu :deep(.el-menu-item.is-active){ background: rgba(77,171,247,0.08); color: var(--kb-accent); }

.recall-layout{ display: grid; grid-template-columns: 1fr 260px; gap: 16px; align-items: start; }
.recall-card{ margin-top: 4px; }
.recall-form{ display: flex; flex-direction: column; gap: 10px; }
.recall-actions{ display: flex; justify-content: flex-end; }
.recall-result{ margin-top: 12px; display: flex; flex-direction: column; gap: 10px; max-height: 60vh; overflow-y: auto; }
.recall-item{ border: 1px dashed var(--el-border-color); border-radius: 10px; padding: 8px 10px; }
.recall-item .q{ font-weight: 700; margin-bottom: 6px; }
.recall-item .hits{ display: grid; grid-template-columns: 1fr; gap: 10px; }
.hit-card{ border: 1px solid var(--el-border-color); border-radius: 10px; padding: 8px 10px; background: var(--el-fill-color-light); position: relative; cursor: pointer; transition: box-shadow .2s ease, border-color .2s ease; }
.hit-card:hover{ border-color: var(--kb-accent); box-shadow: 0 8px 18px rgba(64,158,255,.12); }
.hit-card::before{ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: linear-gradient(180deg, rgba(77,171,247,.6), rgba(77,171,247,0)); border-top-left-radius: 10px; border-bottom-left-radius: 10px; }
.hit-card .hit-head{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; gap: 8px; }
.hit-card .doc{ font-size: 13px; color: var(--el-text-color-regular); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hit-card .score{ color: var(--kb-accent); font-size: 12px; background: rgba(77,171,247,0.12); padding: 2px 8px; border-radius: 999px; font-weight: 700; }
.hit-card .chunk{ font-size: 13px; color: var(--el-text-color-secondary); line-height: 1.6; white-space: pre-wrap; }

.recall-settings{ position: sticky; top: 16px; }
.settings-stack{ display: flex; flex-direction: column; gap: 12px; }
.settings-title{ font-weight: 700; font-size: 14px; margin-bottom: 6px; }
.settings-form{ display: flex; flex-direction: column; gap: 8px; }
.history-title{ font-weight: 700; font-size: 14px; margin-bottom: 6px; }
.history-list{ display: flex; flex-direction: column; gap: 8px; max-height: 40vh; overflow-y: auto; padding-right: 4px; }
.history-item{ padding: 10px 12px; border-radius: 12px; border: 1px solid var(--el-border-color); background: #fff; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 10px rgba(16,24,40,.04); transition: all .2s ease; }
.history-item:hover{ border-color: var(--kb-accent); box-shadow: 0 10px 22px rgba(64,158,255,.12); }
.history-left{ display: flex; align-items: center; gap: 10px; cursor: pointer; min-width: 0; }
.history-avatar{ width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: rgba(77,171,247,.12); border: 1px solid var(--el-border-color); font-size: 14px; }
.history-main{ display: flex; flex-direction: column; min-width: 0; }
.history-q{ font-size: 13px; color: var(--el-text-color-regular); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
.history-sub{ font-size: 12px; color: var(--el-text-color-secondary); margin-top: 2px; }
.history-del{ align-self: flex-start; }
.history-empty{ font-size: 12px; color: var(--el-text-color-secondary); text-align: center; }

.recall-empty{ font-size: 13px; color: var(--el-text-color-secondary); text-align: center; padding: 12px 0; }

.kb-main{ min-width: 0; }

.head{ display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.head h3{ font-size: 22px; background-image: linear-gradient(120deg, rgba(77,171,247,0.35), rgba(77,171,247,0)); background-size: 100% 40%; background-repeat: no-repeat; background-position: 0 80%; }
.spacer{ flex: 1; }
.add-doc-btn{ display: inline-flex; align-items: center; gap: 8px; height: 38px; padding: 0 18px; border-radius: 999px; font-weight: 700; box-shadow: 0 10px 24px rgba(61,85,236,.28); }
.add-doc-btn :deep(.el-icon){ font-size: 16px; }
.add-doc-btn:hover{ filter: brightness(1.05); box-shadow: 0 12px 28px rgba(61,85,236,.32); }
.kb-info{ margin-bottom: 16px; border: 1px solid var(--el-border-color); border-radius: 14px; transition: box-shadow .28s ease, border-color .28s ease; background: var(--el-fill-color-blank); }
.kb-info:hover{ border-color: var(--kb-accent); box-shadow: 0 12px 28px rgba(64, 158, 255, 0.14); }
.kb-info .kb-title{ font-weight: 800; font-size: 18px; }
.kb-info .kb-desc{ color: var(--el-text-color-secondary); font-size: 14px; margin-top: 4px; }
.kb-stats{ display: flex; gap: 16px; margin-top: 12px; }
.kb-stats .stat{ background: var(--el-fill-color-light); border: 1px solid var(--el-border-color); border-radius: 12px; padding: 12px 16px; min-width: 200px; transition: border-color .24s ease, box-shadow .24s ease; }
.kb-stats .stat:hover{ border-color: var(--kb-accent); box-shadow: 0 10px 22px rgba(64, 158, 255, 0.12); }
.kb-stats .stat .label{ color: var(--el-text-color-secondary); font-size: 13px; }
.kb-stats .stat .value{ font-weight: 800; margin-top: 6px; font-size: 18px; }

/* Table enhancement */
:deep(.el-table){ border-radius: 12px; overflow: hidden; }
:deep(.el-table__header th){ background: rgba(77,171,247,0.08); color: var(--el-text-color-primary); font-weight: 700; }
:deep(.el-table .cell){ font-size: 14px; }
:deep(.el-table__row--striped .el-table__cell){ background-color: rgba(77,171,247,0.03); }
:deep(.el-table__body tr:hover>td){ background-color: rgba(77,171,247,0.07) !important; }

.pager{ display: flex; justify-content: center; margin-top: 16px; }
.doc-name{ cursor: text; }

/* Inputs/buttons */
:deep(.el-input__inner){ font-size: 14px; }
:deep(.el-input__wrapper.is-focus){ box-shadow: 0 0 0 3px rgba(77,171,247,0.12); border-color: var(--kb-accent); }

/* AI-style buttons */
.kb-docs-page :deep(.el-button){
  background-color: var(--accent, var(--kb-accent));
  border: none;
  color: #ffffff;
  box-shadow: 0 6px 18px rgba(61,85,236,.25);
}
.kb-docs-page :deep(.el-button:hover){ filter: brightness(1.05); }
.kb-docs-page :deep(.el-button.is-link){
  background-color: var(--accent, var(--kb-accent));
  color: #ffffff;
  padding: 6px 12px;
  border-radius: 6px;
}
:deep(.el-popconfirm) .el-button--danger{ filter: brightness(0.95); }

/* Enter animation */
.kb-docs-page{ animation: fadeIn .28s ease both; }
@keyframes fadeIn{ from{ opacity:0; transform: translateY(6px);} to{ opacity:1; transform: translateY(0);} }

/* Responsive stats wrap */
@media (max-width: 720px){ .kb-stats{ flex-wrap: wrap; } }
</style> 